Skip to contents

This vignette demonstrates the complete workflow for generating bike bus routes from origin-destination data using the bicischools package. We’ll walk through the process using data from Almada, Portugal, showing how to go from raw geographic data to optimised bike bus routes.

Overview

The workflow consists of several key steps:

  1. Data Preparation: Load and process geographic zones and school locations
  2. Origin-Destination Simulation: Use gravity models to estimate student trips
  3. Route Generation: Create cycling routes between origins and destinations
  4. Network Analysis: Build route networks and calculate cycling uptake
  5. Route Optimisation: Identify and optimise bike bus routes
  6. Visualisation: Map results for analysis

1. Data Preparation

We’ll use census zones (BGRI) as origins and schools as destinations. First, let’s load the included sample data:

# Load sample data included with the package
data("origins_almada")
data("schools_lisbon")
data("od_data_almada")
data("routes_almada")

# Display basic information
cat("Number of origin zones:", nrow(origins_almada), "\n")
#> Number of origin zones: 181
cat("Number of schools:", nrow(schools_lisbon), "\n")
#> Number of schools: 52
cat("Number of OD pairs:", nrow(od_data_almada), "\n")
#> Number of OD pairs: 300

Let’s visualise the basic geography:

tm_shape(origins_almada) +
    tm_borders() +
    tm_shape(schools_lisbon) +
    tm_dots("red", size = 0.5) +
    tm_title("Origins and Schools in Almada")

2. Origin-Destination Flow Simulation

For this example, we’ll use pre-calculated OD data, but here’s how you would generate it using a gravity model:

# Example gravity model simulation (not run with sample data)
results <- sim_schools(
    origins = origins_almada,
    destinations = schools_lisbon,
    model = "gravity",
    max_dist = 3000,
    balancing = "destinations",
    d = distance_euclidean,
    m = origin_pupils,
    n = destination_n_pupils,
    constraint_production = origin_pupils,
    beta = 0.99944,
    keep_cols = TRUE
)

3. Route Generation

Generate cycling routes between origins and the selected school:

# The routes are already generated in our sample data
# Here's how you would generate them:
# routes_almada <- bici_routes(od_data_almada, distance.threshold = 5e3)

# Visualise the routes
tm_shape(origins_almada) +
    tm_borders() +
    tm_shape(od_data_almada) +
    tm_lines(col = "grey", col_alpha = 0.3) +
    tm_shape(routes_almada) +
    tm_lines(col = "dodgerblue", col_alpha = 0.6, lwd = 2) +
    tm_shape(schools_lisbon) +
    tm_dots("red", size = 0.5) +
    tm_title("Cycling Routes to School")
#> Registered S3 method overwritten by 'jsonify':
#>   method     from    
#>   print.json jsonlite

4. Network Analysis and Cycling Uptake

Calculate potential cycling uptake using Propensity to Cycle Tool (PCT) models and create a route network:

# Calculate cycling uptake potential
routes_uptake <- routes_pct_uptake(routes_almada)

# Create route network
rnet <- routes_network(routes_uptake)
#> 2025-09-01 11:14:03.243846 constructing segments
#> 2025-09-01 11:14:03.347023 building geometry
#> 2025-09-01 11:14:03.381095 simplifying geometry
#> 2025-09-01 11:14:03.381687 aggregating flows
#> 2025-09-01 11:14:03.489877 rejoining segments into linestrings

# Summarise routes by origin
routes_summaries <- summarise_routes(routes_uptake)

cat("Routes with cycling potential:", nrow(routes_uptake), "\n")
#> Routes with cycling potential: 3350
cat("Network segments:", nrow(rnet), "\n")
#> Network segments: 551

Visualise the network with cycling potential:

tm_shape(rnet) +
    tm_lines(
        col = "bicycle_godutch",
        col.scale = tm_scale(palette = "viridis"),
        lwd = 3,
        col.legend = tm_legend("Potential Cyclists")
    ) +
    tm_shape(schools_lisbon) +
    tm_dots(col = "red", size = 0.3) +
    tm_title("Cycling Network with Uptake Potential")

5. Bike Bus Route Identification

Identify optimal bike bus routes using the route bundling algorithm:

# Generate ordered bike bus routes
ordered_routes <- cycle_bus_routes(
    routes_summaries,
    rnet = rnet,
    min_trips = 1,
    buffer = 10
)
#> Warning: attribute variables are assumed to be spatially constant throughout
#> all geometries

# Calculate route statistics
route_stats <- calc_stats(
    routes = routes_summaries,
    rnet_plan = rnet,
    ordered_routes = ordered_routes,
    min_trips = 1
)
#> Warning: attribute variables are assumed to be spatially constant throughout
#> all geometries

# Filter to top routes
top_routes <- filter_routes(ordered_routes, buffer = 300, top_n = 3)
#> Warning in st_cast.sf(routes_sorted, "POINT"): repeating attributes for all
#> sub-geometries for which they may not be constant

cat("Total potential routes:", nrow(ordered_routes), "\n")
#> Total potential routes: 134
cat("Top priority routes:", nrow(top_routes), "\n")
#> Top priority routes: 3

6. Centroid Matching and Final Selection

Match student origins to the top bike bus routes:

# Match centroids to bike bus routes
centroids <- match_centroids(
    routes_cents = ordered_routes,
    top_routes = top_routes,
    route_stats = route_stats,
    origins = origins_almada,
    id.col = "id",
    origin_route.id = "O",
    origin.id = "id",
    attribute_trips = "bicycle_godutch",
    max_dist_to_bikebus = 300,
    min_dist_threshold = 500
)
#> Warning in st_cast.sf(routes_cents_clean, "POINT"): repeating attributes for
#> all sub-geometries for which they may not be constant

cat("Students assigned to bike bus routes:", nrow(centroids), "\n")
#> Students assigned to bike bus routes: 130

7. Final Visualisation

Create a comprehensive map showing the final bike bus network:

# Create final visualisation
tm_shape(origins_almada) +
    tm_borders(col_alpha = 0.2) +
    tm_shape(centroids) +
    tm_bubbles(
        fill = "pick",
        size = "bicycle_godutch",
        fill.scale = tm_scale_categorical(values = "brewer.set1"),
        fill.legend = tm_legend_hide(),
        size.legend = tm_legend(title = "Potential Cyclists")
    ) +
    tm_shape(top_routes %>% mutate(route_id = as.character(row_number()))) +
    tm_lines(
        col = "route_id",
        col.scale = tm_scale_categorical(values = "brewer.set1"),
        lwd = 4,
        col.legend = tm_legend(title = "Bike Bus Routes")
    ) +
    tm_shape(schools_lisbon) +
    tm_dots(size = 0.5, shape = 17) +
    tm_title("Optimised Bike Bus Routes for School Travel")